package file

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/apulis/bmod/file-server/configs"
	"github.com/apulis/bmod/file-server/internal/constant"
	"github.com/apulis/bmod/file-server/internal/dto"
	"github.com/apulis/sdk/go-utils/logging"
	"github.com/tus/tusd/pkg/filestore"
	tusd "github.com/tus/tusd/pkg/handler"
	"github.com/tus/tusd/pkg/memorylocker"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

var (
	store filestore.FileStore
)

func NewHandler() *tusd.UnroutedHandler {
	config := configs.Config

	// if error exist, then exit
	if err := prepareDir(config.FileStore.MountPath); err != nil {
		logging.Fatal().Err(err).Str("dir", config.FileStore.MountPath).Msg("init directory error")
	}

	store = filestore.FileStore{
		Path: config.FileStore.MountPath,
	}
	lock := memorylocker.New()

	composer := tusd.NewStoreComposer()
	store.UseIn(composer)
	lock.UseIn(composer)

	// The StoreComposer property must be set to allow the handler to function.
	var handler *tusd.UnroutedHandler
	handler, err := tusd.NewUnroutedHandler(tusd.Config{
		BasePath:              configs.Config.Prefix,
		MaxSize:               configs.Config.MaxSize,
		StoreComposer:         composer,
		NotifyCompleteUploads: true,
		PreFinishResponseCallback: func(hook tusd.HookEvent) error {
			return finishResponseCallback(handler, hook)
		},
	})
	if err != nil {
		logging.Fatal().Err(err).Msg("tusd init file handler error")
		return nil
	}

	go tusdNotify(handler)

	return handler
}

func prepareDir(path string) error {
	fi, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			if err := os.MkdirAll(path, 0775); err != nil {
				logging.Error(err).Str("path", path).Msg("create directory error")
				return err
			}
			return nil
		} else {
			return err
		}
	}
	if !fi.IsDir() {
		return errors.New(fmt.Sprintf("%s is not directory, plz check", path))
	}

	return nil
}

func tusdNotify(handler *tusd.UnroutedHandler) {
	config := configs.Config
	for {
		select {
		case event := <-handler.CompleteUploads:
			status := ""
			if v, ok := event.Upload.MetaData["status"]; ok {
				status = v
			}
			if status != "finish" {
				break
			}
			for _, hook := range config.PostFinishHooks {
				if v, ok := event.Upload.MetaData["moduleName"]; ok {
					if strings.ToLower(v) == strings.ToLower(hook.ModuleName) {
						data := dto.HookData{
							Metadata: event.Upload.MetaData,
							StoragePath: fmt.Sprintf("%s/%s", config.FileStore.PvcRootPath,
								event.Upload.Storage["Path"]),
						}
						go notifyHook(event.HTTPRequest.Header, hook.Endpoint, &data)
					}
				}
			}
		}
	}
}

func finishResponseCallback(handler *tusd.UnroutedHandler, event tusd.HookEvent) error {
	logging.Debug().Str("id", event.Upload.ID).
		Str("filename", event.Upload.MetaData["filename"]).
		Msg("finish response callback")
	location := ""
	if v, ok := event.Upload.MetaData["moduleName"]; ok {
		location = v
	}
	location = strings.TrimPrefix(location, "/")
	if len(location) == 0 {
		constant.ErrorMissingField.Message = "missing moduleName"
		return constant.ErrorMissingField
	}

	if err := prepareDir(filepath.Join(configs.Config.FileStore.MountPath, location)); err != nil {
		logging.Error(err).Str("dir", location).Msg("init directory error")
		constant.ErrorCreateDirectory.Message = err.Error()
		return constant.ErrorCreateDirectory
	}

	fileName := ""
	if v, ok := event.Upload.MetaData["filename"]; ok {
		fileName = v
	}
	if len(fileName) == 0 {
		constant.ErrorMissingField.Message = "missing filename"
		return constant.ErrorMissingField
	}

	origPath := ""
	if v, ok := event.Upload.Storage["Path"]; ok {
		origPath = v
	}
	if len(origPath) == 0 {
		logging.Debug().Msg("storage path is empty")
		constant.ErrorMissingField.Message = "missing storage path"
		return constant.ErrorMissingField
	}

	newPath := filepath.Join(location, fileName)
	if err := os.Rename(origPath, filepath.Join(configs.Config.FileStore.MountPath, newPath)); err != nil {
		logging.Error(err).Str("src", origPath).Str("dest", newPath).Msg("move file error")
		constant.ErrorFileMove.Message = err.Error()
		return constant.ErrorFileMove
	}

	event.Upload.Storage["Path"] = newPath
	if err := updateFileInfo(handler, event.Upload); err != nil {
		logging.Error(err).Str("id", event.Upload.ID).Str("path", newPath).
			Msg("update storage path failed")
		constant.ErrorUpdateFileInfo.Message = err.Error()
		return constant.ErrorUpdateFileInfo
	}
	logging.Debug().Str("id", event.Upload.ID).Str("origPath", origPath).
		Str("destPath", newPath).Msg("move file successfully")
	event.Upload.MetaData["status"] = "finish"

	handler.CompleteUploads <- event

	return nil
}

func notifyHook(header http.Header, url string, hookData *dto.HookData) {
	var (
		err      error
		response *http.Response
		retries  = 3
	)

	data, _ := json.Marshal(hookData)
	req, _ := http.NewRequest("POST", url, bytes.NewBuffer(data))
	for key, values := range header {
		for _, value := range values {
			req.Header.Set(key, value)
		}
	}
	req.Header.Set("Content-Type", "application/json")
	for retries > 0 {
		response, err = (&http.Client{}).Do(req)
		if err != nil {
			logging.Error(err).Int("retry", retries).Str("hook", url).Msg("post to hook error")
			retries -= 1
		} else {
			break
		}
	}
	if err != nil {
		logging.Error(err).Str("hook", url).Msg("post to hook exceed retry count")
		return
	}
	if response == nil {
		logging.Error(errors.New("empty response")).Str("hook", url).Msg("no response from hook")
		return
	}
	defer response.Body.Close()

	logging.Debug().Str("hook", url).Msg("call webhook success")
}

func updateFileInfo(handler *tusd.UnroutedHandler, info tusd.FileInfo) error {
	infoFile := filepath.Join(store.Path, info.ID+".info")

	data, _ := json.Marshal(info)
	if err := ioutil.WriteFile(infoFile, data, os.FileMode(0664)); err != nil {
		logging.Error(err).Str("info", infoFile).Msg("dump info file error")
		return err
	}
	return nil
}
